﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Garfield.Core.Runtime.SPI;
using PicoContainer;
using PicoContainer.Defaults;

namespace Garfield.Runtime.Pico
{
    internal class ComponentAdapter : ConstructorInjectionComponentAdapter
    {
        private readonly PlatformProvider provider;

        public ComponentAdapter(PlatformProvider provider, object componentKey, Type componentImplementation,
                                 IParameter[] parameters)
            : base(componentKey, componentImplementation, parameters)
        {
            this.provider = provider;
        }

        public Delegate Initializer { private get; set;}

        public override object GetComponentInstance(IPicoContainer container)
        {
            var greediestSatisfiableConstructor = GetGreediestSatisfiableConstructor(container);
            var instance = greediestSatisfiableConstructor.Invoke(GetConstructorArguments(container, greediestSatisfiableConstructor));
            var injectedInstance = InjectDependencies(instance, instance.GetType());
            if (Initializer != null)
                Initializer.DynamicInvoke(injectedInstance);
            return injectedInstance;
        }

        protected override ConstructorInfo GetGreediestSatisfiableConstructor(IPicoContainer container)
        {
            var constructors =
                ComponentImplementation.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length);

            var satisfiableConstructor = constructors.FirstOrDefault(constructor => AreAllParametersDefinedInContainer(constructor.GetParameters(), container));
            if (satisfiableConstructor == null)
            {
                throw new UnsatisfiableDependenciesException("Can't found proper constructor for component " + ComponentImplementation);
            }
            return satisfiableConstructor;
        }

        protected new object[] GetConstructorArguments(IPicoContainer container, ConstructorInfo ctor)
        {
            return ctor.GetParameters().Select(parameter => GetParameterValue(parameter, container)).ToArray();
        }

        private object GetParameterValue(ParameterInfo parameter, IPicoContainer container)
        {
            return parameter.Has<IDependencyAttribute>() ? GetDependencyParameterValue(parameter) : container.GetComponentInstance(parameter.ParameterType);
        }

        private object GetDependencyParameterValue(ParameterInfo parameter)
        {
            return parameter.Get<IDependencyAttribute>()[0].ProvideValue(provider, parameter.ParameterType);
        }

        private static bool AreAllParametersDefinedInContainer(IEnumerable<ParameterInfo> parameters, IPicoContainer container)
        {
            return parameters.All(parameter => parameter.Has<IDependencyAttribute>() || container.GetComponentAdapter(parameter.ParameterType) != null);
        }

        private object InjectDependencies(object instance, Type type)
        {
            if (type.BaseType != null && !type.BaseType.Namespace.StartsWith("System."))
            {
                InjectDependencies(instance, type.BaseType);
            }

            var publicOrNonPublicInstance = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
            foreach(var field in type.GetFields<IDependencyAttribute>(publicOrNonPublicInstance))
            {
                field.SetValue(instance, field.Get<IDependencyAttribute>()[0].ProvideValue(provider, field.FieldType));
            }

            foreach (var property in type.GetProperties<IDependencyAttribute>(publicOrNonPublicInstance))
            {
                if (property.GetSetMethod(true) != null)
                    property.SetValue(instance, property.Get<IDependencyAttribute>()[0].ProvideValue(provider, property.PropertyType), null);
            }
            return instance;
        }
    }
}
